# https://www.geeksforgeeks.org/fractal-using-spirograph-python/

# importing the required libraries
import os
import random, argparse
import math
import turtle
from datetime import datetime


# A class that draws a spirograph
class Spiro:
    # constructor
    def __init__(self, xc, yc, col, R, r, l):

        # create own turtle
        self.t = turtle.Turtle()
        # set cursor shape
        self.t.shape('turtle')
        # set step in degrees
        self.step = 5
        # set drawing complete flag
        self.drawingComplete = False

        # set parameters
        self.setparams(xc, yc, col, R, r, l)

        # initiatize drawing
        self.restart()

        # set parameters

    def setparams(self, xc, yc, col, R, r, l, maxRot=5):
        # spirograph parameters
        self.xc = xc
        self.yc = yc
        self.R = int(R)
        self.r = int(r)
        self.l = l
        self.col = col
        # reduce r/R to smallest form by dividing with GCD
        gcdVal = math.gcd(self.r, self.R)
        self.nRot = min(maxRot, self.r // gcdVal)
        # get ratio of radii
        self.k = r / float(R)
        # set color
        self.t.color(*col)
        # current angle
        self.a = 0

    # restart drawing
    def restart(self):
        # set flag
        self.drawingComplete = False
        # show turtle
        self.t.showturtle()
        # go to first point
        self.t.up()
        R, k, l = self.R, self.k, self.l
        a = 0.0
        x = R * ((1 - k) * math.cos(a) + l * k * math.cos((1 - k) * a / k))
        y = R * ((1 - k) * math.sin(a) - l * k * math.sin((1 - k) * a / k))
        self.t.setpos(self.xc + x, self.yc + y)
        self.t.down()

    def draw(self):
        # draw the whole thing
        # draw rest of points
        R, k, l = self.R, self.k, self.l
        for i in range(0, 360 * self.nRot + 1, self.step):
            a = math.radians(i)
            x = R * ((1 - k) * math.cos(a) + l * k * math.cos((1 - k) * a / k))
            y = R * ((1 - k) * math.sin(a) - l * k * math.sin((1 - k) * a / k))
            self.t.setpos(self.xc + x, self.yc + y)
            # done - hide turtle
        self.t.hideturtle()

        # update by one step

    def update(self):
        # skip if done
        if self.drawingComplete:
            return

        # increment angle
        self.a += self.step

        # draw step
        R, k, l = self.R, self.k, self.l

        # set angle
        a = math.radians(self.a)
        x = self.R * ((1 - k) * math.cos(a) + l * k * math.cos((1 - k) * a / k))
        y = self.R * ((1 - k) * math.sin(a) - l * k * math.sin((1 - k) * a / k))
        self.t.setpos(self.xc + x, self.yc + y)

        # check if drawing is complete and set flag
        if self.a >= 360 * self.nRot:
            self.drawingComplete = True
            # done - hide turtle
            self.t.hideturtle()


    def clear(self):
        # clear everything
        self.t.clear()



class SpiroAnimator:
    # A class for animating spirographs

    def __init__(self, N):
        # constructor
        self.finished = False

        # timer value in milliseconds
        self.deltaT = 10

        # get window dimensions
        self.width = turtle.window_width()
        self.height = turtle.window_height()

        # create spiro objects
        self.spiros = []
        for i in range(N):
            # generate random parameters
            rparams = self.genRandomParams(i)
            # set spiro params
            spiro = Spiro(*rparams)
            self.spiros.append(spiro)

        # call timer
        #turtle.ontimer(self.update, self.deltaT)


    def restart(self):
        # restart sprio drawing
        i = 0
        for spiro in self.spiros:
            # clear
            spiro.clear()
            # generate random parameters
            rparams = self.genRandomParams(i)
            # set spiro params
            spiro.setparams(*rparams)
            # restart drawing
            spiro.restart()

            i = i+1


    def genRandomParams(self, idx):
        # generate random parameters
        width, height = self.width, self.height
        R = random.randint(50, min(width, height) // 2)
        r = random.randint(10, 9 * R // 10)
        l = random.uniform(0.1, 0.9)

        xc = -width/4  + (idx%2)*width/2
        yc = -height/4 + (idx//2)*height/2
        #xc = random.randint(-width // 2, width // 2)
        #yc = random.randint(-height // 2, height // 2)

        col = (random.random(),
               random.random(),
               random.random())
        return (xc, yc, col, R, r, l)

    def update(self):
        # update all spiros
        nComplete = 0
        for spiro in self.spiros:
            # update
            spiro.update()
            # count completed ones
            if spiro.drawingComplete:
                nComplete += 1

        # if all spiros are complete, restart
        if nComplete == len(self.spiros):
            self.finished = True

        #turtle.ontimer(self.update, self.deltaT)


    def toggleTurtles(self):
        for spiro in self.spiros:
            if spiro.t.isvisible():
                spiro.t.hideturtle()
            else:
                spiro.t.showturtle()

    def waitFinished(self):
        while not self.finished:
            self.update()
            #time.sleep(1)

def convertImage(fileName):
    try:
        from PIL import Image

        img = Image.open(fileName + '.eps')
        img.save(fileName + '.png', 'png')
    except:
        cmd = "convert %s %s" % (fileName+".eps", fileName+".png")
        os.system(cmd)

def saveDrawing():
    # save spiros to image

    # hide turtle
    turtle.hideturtle()
    # generate unique file name
    dateStr = (datetime.now()).strftime("%d%b%Y-%H%M%S")
    fileName = 'spiro-' + dateStr
    print('saving drawing to %s.eps/png' % fileName)
    # get tkinter canvas
    canvas = turtle.getcanvas()
    # save postscipt image
    canvas.postscript(file=fileName + '.eps')
    # use PIL to convert to PNG
    convertImage(fileName)

    # show turtle
    turtle.showturtle()


# main() function
def draw():
    # use sys.argv if needed
    print('generating spirograph...')

    # create parser
    descStr = """This program draws spirographs using the Turtle module.  
    When run with no arguments, this program draws random spirographs. 

    Terminology: 

    R: radius of outer circle. 
    r: radius of inner circle. 
    l: ratio of hole distance to r. 
    """
    parser = argparse.ArgumentParser(description=descStr)

    # add expected arguments
    parser.add_argument('--sparams', nargs=3, dest='sparams', required=False,
                        help="The three arguments in sparams: R, r, l.")

    # parse args
    args = parser.parse_args()

    # set to 80% screen width
    turtle.setup(width=0.8)

    # set cursor shape
    turtle.shape('turtle')

    # set title
    #turtle.title("Spirographs!")

    # add key handler for saving images
    turtle.onkey(saveDrawing, "s")
    # start listening
    turtle.listen()

    # hide main turtle cursor
    turtle.hideturtle()

    # checks args and draw
    if args.sparams:
        params = [float(x) for x in args.sparams]
        # draw spirograph with given parameters
        # black by default
        col = (0.0, 0.0, 0.0)
        spiro = Spiro(0, 0, col, *params)
        spiro.draw()
    else:
        # create animator object
        spiroAnim = SpiroAnimator(4)

        # add key handler to toggle turtle cursor
        turtle.onkey(spiroAnim.toggleTurtles, "t")
        # add key handler to restart animation
        turtle.onkey(spiroAnim.restart, "space")

        # wait all spirograph finished
        spiroAnim.waitFinished()


# call main
if __name__ == '__main__':
    draw()

    # start turtle main loop
    turtle.mainloop()